use futures::StreamExt;
use grpc_gen::inventory_client::InventoryClient;
use grpc_gen::{
    Item, ItemIdentifier, ItemInformation, ItemStock, PriceChangeRequest, QuantityChangeRequest,
};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct Options {
    pub command: Command,
}

#[derive(Debug, Serialize, Deserialize)]
pub enum Command {
    Add(AddOptions),
    Remove(RemoveOptions),
    Get(GetOptions),
    UpdateQuantity(UpdateQuantityOptions),
    UpdatePrice(UpdatePriceOptions),
    Watch(GetOptions),
}

#[derive(Debug, Serialize, Deserialize)]
pub struct AddOptions {
    sku: String,
    price: f32,
    quantity: u32,
    name: Option<String>,
    description: Option<String>,
}

pub async fn add(opts: AddOptions) -> Result<String, Box<dyn std::error::Error>> {
    let mut client = InventoryClient::connect("http://127.0.0.1:9001").await?;

    let id = ItemIdentifier { sku: opts.sku };

    let stock = ItemStock {
        price: opts.price,
        quantity: opts.quantity,
    };

    let info = ItemInformation {
        name: opts.name,
        description: opts.description,
    };

    let item = Item {
        identifier: Some(id),
        stock: Some(stock),
        information: Some(info),
    };

    let request = tonic::Request::new(item);
    let response = client.add(request).await?;
    assert_eq!(response.into_inner().status, "success");
    Ok("success: item was added to the inventory.".to_string())
}

#[derive(Debug, Serialize, Deserialize)]
pub struct RemoveOptions {
    sku: String,
}

pub async fn remove(opts: RemoveOptions) -> Result<String, Box<dyn std::error::Error>> {
    let mut client = InventoryClient::connect("http://127.0.0.1:9001").await?;

    let request = tonic::Request::new(ItemIdentifier { sku: opts.sku });
    let response = client.remove(request).await?;
    let msg = response.into_inner().status;
    assert!(msg.starts_with("success"));
    Ok(format!("{}", msg))
}

#[derive(Debug, Serialize, Deserialize)]
pub struct GetOptions {
    sku: String,
}

pub async fn get(opts: GetOptions) -> Result<String, Box<dyn std::error::Error>> {
    let mut client = InventoryClient::connect("http://127.0.0.1:9001").await?;

    let request = tonic::Request::new(ItemIdentifier { sku: opts.sku });
    let item = client.get(request).await?.into_inner();
    Ok(format!("found item: {:?}", item))
}

#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateQuantityOptions {
    sku: String,
    change: i32,
}

pub async fn update_quantity(
    opts: UpdateQuantityOptions,
) -> Result<String, Box<dyn std::error::Error>> {
    let mut client = InventoryClient::connect("http://127.0.0.1:9001").await?;

    let request = tonic::Request::new(QuantityChangeRequest {
        sku: opts.sku,
        change: opts.change,
    });

    let message = client.update_quantity(request).await?.into_inner();
    assert_eq!(message.status, "success");
    Ok(format!(
        "success: quantity was updated. Quantity: {} Price: {}",
        message.quantity, message.price
    ))
}

#[derive(Debug, Serialize, Deserialize)]
pub struct UpdatePriceOptions {
    sku: String,
    price: f32,
}

pub async fn update_price(opts: UpdatePriceOptions) -> Result<String, Box<dyn std::error::Error>> {
    let mut client = InventoryClient::connect("http://127.0.0.1:9001").await?;

    let request = tonic::Request::new(PriceChangeRequest {
        sku: opts.sku,
        price: opts.price,
    });

    let message = client.update_price(request).await?.into_inner();
    assert_eq!(message.status, "success");
    Ok(format!(
        "success: price was updated. Quantity: {} Price: {}",
        message.quantity, message.price
    ))
}

pub async fn watch(opts: GetOptions) -> Result<String, Box<dyn std::error::Error>> {
    let mut client = InventoryClient::connect("http://127.0.0.1:9001").await?;

    let mut stream = client
        .watch(ItemIdentifier {
            sku: opts.sku.clone(),
        })
        .await?
        .into_inner();
    let mut buf = String::new();
    buf.push_str("item was updated: ");
    println!("streaming changes to item {}", opts.sku);
    while let Some(item) = stream.next().await {
        match item {
            Ok(item) => buf.push_str(&format!("{:?} ", item)),
            Err(err) => {
                if err.code() == tonic::Code::NotFound {
                    println!("watched item has been removed from the inventory.");
                    break;
                } else {
                    return Err(err.into());
                }
            }
        };
    }
    println!("stream closed");

    Ok(buf)
}
